package ru.ifmo.croak.util;

import ru.ifmo.croak.api.PostService;
import ru.ifmo.croak.api.QueryService;

import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.NoSuchElementException;
import java.util.Properties;
import java.util.concurrent.*;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Helper class to work with configuration properties and to lookup service implementation.
 *
 * @author Roman Elizarov
 */
public class CroakFactory {
    /**
     * Name of the configuration properties file.
     */
    public static final String CROAK_PROPERTIES = "croak.properties";

    private static final Logger log = Logger.getLogger(CroakFactory.class.getName());
    private static final CroakFactory INSTANCE = new CroakFactory();

    /**
     * Returns instance of the {@link CroakFactory}.
     */
    public static CroakFactory getInstance() {
        return INSTANCE;
    }

    private final Properties properties = new Properties(System.getProperties());
    private final ConcurrentMap<String, ImplHolder> impl = new ConcurrentHashMap<String, ImplHolder>();

    private CroakFactory() {
        try {
            properties.load(new FileInputStream(CROAK_PROPERTIES));
        } catch (IOException e) {
            log.log(Level.WARNING, "Cannot load " + CROAK_PROPERTIES + " file.", e);
        }
    }

    /**
     * Returns value of the property with the given key from the {@link #CROAK_PROPERTIES configuration file}.
     *
     * @throws NoSuchElementException if the property value for the corresponding key is not found.
     */
    public String getProperty(String key) {
        String result = properties.getProperty(key);
        if (result == null)
            throw new NoSuchElementException("Key " + key + " is not found.");
        return result;
    }

    /**
     * Starts a main class of the service with the specified name.
     * This method reads class name from {@code <name>.main.class} {@link #getProperty(String) property}.
     * The resulting class is supposed to implement {@link Runnable} interface, which is then
     * invoked in a separate thread.
     */
    public void start(String name) {
        new Thread(getImplementation(Runnable.class, name), name).start();
    }

    /**
     * Returns implementation of the corresponding service's interface with a given name.
     * This method reads class name from {@code <name>.<tag>.class} {@link #getProperty(String) property}, where tag
     * depends on {@code intf}:
     * <ul>
     * <li> For {@link PostService} tag is {@code post}.
     * <li> For {@link QueryService} tag is {@code query}.
     * <li> For {@link Runnable} tag is {@code main}.
     * </ul>
     */
    @SuppressWarnings({"unchecked"})
    public <T> T getImplementation(Class<T> intf, String name) {
        String key;
        if (intf == PostService.class)
            key = name + ".post.class";
        else if (intf == QueryService.class)
            key = name + ".query.class";
        else if (intf == Runnable.class)
            key = name + ".main.class";
        else
            throw new IllegalArgumentException("Unsupported interface " + intf.getName());
        Object result = getImplInternal(name, key);
        if (!intf.isInstance(result))
            throw new IllegalArgumentException("Service " + key +
                    " implementation class " + result.getClass().getName() +
                    " does not implement " + intf.getName() + " interface.");
        return (T) result;
    }

    private Object getImplInternal(String name, String key) {
        ImplHolder holder = impl.get(getProperty(key));
        if (holder != null)
            return holder.getImpl();
        holder = new ImplHolder(name, key);
        ImplHolder prev = impl.putIfAbsent(getProperty(key), holder);
        if (prev != null)
            holder = prev;
        else
            holder.createImpl();
        return holder.getImpl();
    }

    private class ImplHolder implements Callable<Object> {
        private final String name;
        private final String key;
        private final FutureTask<Object> future;

        private ImplHolder(String name, String key) {
            this.name = name;
            this.key = key;
            future = new FutureTask<Object>(this);
        }

        public void createImpl() {
            future.run();
        }

        public Object getImpl() {
            try {
                return future.get();
            } catch (InterruptedException e) {
                throw new IllegalStateException("Service " + key + " creation was interrupted.", e);
            } catch (ExecutionException e) {
                throw new RuntimeException("Service " + key + " creation failed with exception.", e.getCause());
            }
        }

        public Object call() throws Exception {
            String className = getProperty(key);
            if (className == null)
                throw new IllegalArgumentException("Property " + key + " is not found in " + CROAK_PROPERTIES + " file.");
            Class<?> clazz;
            try {
                clazz = Class.forName(className);
            } catch (ClassNotFoundException e) {
                throw new IllegalArgumentException("Class " + className + " is not found.", e);
            }
            Constructor<?> constructor;
            try {
                constructor = clazz.getConstructor(String.class);
            } catch (NoSuchMethodException e) {
                throw new IllegalArgumentException("Class " + className + " does not have a constructor with a single String name argument.", e);
            }
            Object result;
            try {
                result = constructor.newInstance(name);
            } catch (InstantiationException e) {
                throw new IllegalArgumentException("Class " + className + " is abstract and cannot be instantiated.", e);
            } catch (IllegalAccessException e) {
                throw new IllegalArgumentException("Class " + className + " constructor with a single String name argument cannot be accessed.", e);
            } catch (InvocationTargetException e) {
                throw new RuntimeException("Class " + className + " constructor with a single String name argument failed with exception.", e.getCause());
            }
            log.info("Created service " + key + " implementation with " + className + " class.");
            return result;
        }
    }
}
